﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace WhatsPlayingClient.Audio
{
	public class FreqBand
	{
		public readonly double From;
		public readonly double To;

		public FreqBand(double from, double to)
		{
			From = from;
			To = to;
		}

		public override string ToString()
		{
			return string.Format("{0:F1} - {1:F1} Hz", From, To);
		}
	}

	public class BandSet
	{
		static public readonly BandSet[] Presets = new BandSet[] {
//			BandSet.LinearDivision(10, 20000, 8),
//			BandSet.LogDivision(10, 10000, 8, 0.1, 0.255),
//			new BandSet(FreqBandBuilder.GetBands(10, 10000, 8, FreqBandBuilder.BandType.MEL), 0.1, 0.256),
//			new BandSet(FreqBandBuilder.GetBands(10, 10000, 8, FreqBandBuilder.BandType.ERBS), 0.1, 0.256),
			new BandSet(FreqBandBuilder.GetBands(200, 5000, 8, FreqBandBuilder.BandType.BARK), 0.1, 0.256)
		};

		public readonly double FrameDeltaTime;
		public readonly double FrameHalfWidth;
		public readonly FreqBand[] FreqBands;
		private const double eps = 1e-6;

		private int lastBand = 0;

		public int Count { get { return FreqBands.Length; } }

		#region Factory methods

		public static BandSet LinearDivision(double from, double to, int count, double frameDeltaTime, double frameHalfWidth)
		{
			var bands = new List<FreqBand>();
			double bw = to - from;
			for (int i = 0; i < count; ++i)
				bands.Add(new FreqBand(from + i * bw / count - eps, from + (i + 1) * bw / count + eps));

			return new BandSet(bands.ToArray(), frameDeltaTime, frameHalfWidth);
		}

		public static BandSet LogDivision(double from, double to, int count, double frameDeltaTime, double frameHalfWidth)
		{
			var bands = new List<FreqBand>();
			double ratio = Math.Log10(to) - Math.Log10(from);
			for (int i = 0; i < count; ++i)
				bands.Add(new FreqBand(from * Math.Pow(10, (ratio * i) / count) - eps, from * Math.Pow(10, (ratio * (i + 1)) / count) + eps));

			return new BandSet(bands.ToArray(), frameDeltaTime, frameHalfWidth);
		}

		#endregion

		public BandSet(FreqBand[] bands, double frameDeltaTime, double frameHalfWidth)
		{
			FreqBands = bands;
			FrameDeltaTime = frameDeltaTime;
			FrameHalfWidth = frameHalfWidth;
		}

		public int GetBandIdx(double freq)
		{
			if (FreqBands[lastBand].From < freq && freq < FreqBands[lastBand].To)
				return lastBand;

			for (int i = 0; i < FreqBands.Length; ++i)
				if (FreqBands[i].From < freq && freq < FreqBands[i].To)
					return lastBand = i;

			return -1;
		}
	}

	public class Frame
	{
		private readonly float[] spectrum;
		private readonly double maxDeltaTime;
		private float[] averageSpectrum;

		public float[] Spectrum
		{
			get
			{
				if (averageSpectrum == null)
				{
					if (SpectrumCount <= 1)
						averageSpectrum = spectrum;
					else
					{
						averageSpectrum = new float[spectrum.Length];
						for (int i = 0; i < averageSpectrum.Length; ++i)
							averageSpectrum[i] = spectrum[i] / SpectrumCount;
					}
				}
				return averageSpectrum;
			}
		}
		public int SpectrumCount { get; private set; }
		public double FrameTime { get; private set; }

		public Frame(double time, double maxDeltaTime, int size)
		{
			this.maxDeltaTime = maxDeltaTime;
			FrameTime = time;
			spectrum = new float[size];
			SpectrumCount = 0;
		}

		public Frame(double time, double maxDeltaTime, float[] spc)
		{
			this.maxDeltaTime = maxDeltaTime;
			FrameTime = time;
			spectrum = spc;
			SpectrumCount = 1;
		}

		public int CompareTime(double time)
		{
			if (time > FrameTime + maxDeltaTime)
				return 1;
			if (time < FrameTime - maxDeltaTime)
				return -1;
			return 0;
		}

		public int Add(double time, float[] spc)
		{
			if(spc.Length != spectrum.Length)
				throw new ArgumentException("spectrum lenght differs from initial spectrum lenght");

			int res = CompareTime(time);
			if (res != 0)
				return res;

			averageSpectrum = null;
			++SpectrumCount;
			for(int i = 0; i < spectrum.Length; ++i)
				spectrum[i] += spc[i];

			return res;
		}

		public override string ToString()
		{
			return string.Format("frame (time {0}/{1}, count {2}, len {3})", 
				FrameTime, maxDeltaTime, SpectrumCount, spectrum.Length);
		}
	}

	public class Spectrum
	{
		public BandSet Params { get; private set; }
		public readonly List<Frame> SpectrumData = new List<Frame>();		// each array is Params.Count, one array for each time window

		public Spectrum(BandSet spcParams)
		{
			Params = spcParams;
		}

		public void Cleanup()
		{
			while (SpectrumData[SpectrumData.Count - 1].SpectrumCount == 0)
				SpectrumData.RemoveAt(SpectrumData.Count - 1);

			//MakeDiffs();
			Normalize();
		}

		public void Normalize()
		{
			foreach (var frame in SpectrumData)
			{
				float sum = 0;
				float clampMin = -50;
				float clampMax = -20;
				for (int i = 0; i < frame.Spectrum.Length; ++i)
				{
					frame.Spectrum[i] = Math.Max(frame.Spectrum[i], clampMin);
					frame.Spectrum[i] = Math.Min(frame.Spectrum[i], clampMax);
					sum += frame.Spectrum[i];
				}

				sum /= frame.Spectrum.Length;
				for (int i = 0; i < frame.Spectrum.Length; ++i)
				{
					frame.Spectrum[i] -= sum;
				}
			}
		}

		public void MakeDiffs()
		{
			for (int i = 1; i < SpectrumData.Count; ++i)
			{
				float[] prev = SpectrumData[i - 1].Spectrum;
				float[] cur = SpectrumData[i].Spectrum;
				for (int j = 0; j < prev.Length; ++j)
					prev[j] = (cur[j] - prev[j]) / (prev[j] + cur[j] + 1e-8f);
			}
			SpectrumData.RemoveAt(SpectrumData.Count - 1);
		}

		public void AddSpectrum(double streamTimeSec, int samplesPerSecond, float[] fftData, int count)
		{
			float[] spectrum = DecimateFreqs(samplesPerSecond, fftData, count);
			if (spectrum == null)
				return;

			SpectrumData.Add(new Frame(streamTimeSec, Params.FrameHalfWidth, spectrum));
			return;

			if (SpectrumData.Count == 0)
				SpectrumData.Add(new Frame(streamTimeSec, Params.FrameHalfWidth, spectrum.Length));

			double dt = Params.FrameDeltaTime;
			while (SpectrumData[SpectrumData.Count - 1].CompareTime(streamTimeSec) >= 0)
				SpectrumData.Add(new Frame(SpectrumData[SpectrumData.Count - 1].FrameTime + dt, Params.FrameHalfWidth, spectrum.Length));

			for (int idx = SpectrumData.Count - 1; idx >= 0; --idx)
				if (SpectrumData[idx].Add(streamTimeSec, spectrum) > 0)
					break;
		}

		private float[] DecimateFreqs2(int samplesPerSecond, float[] fftData, int count)
		{
			short[] counts = new short[Params.Count];
			float[] spectrum = new float[Params.Count];

			for (int i = 1; i < count; ++i)
			{
				double freq = samplesPerSecond * i / count / 2;
				int bandIdx = Params.GetBandIdx(freq);
				if (bandIdx >= 0 && counts[bandIdx] == 0)
				{
					spectrum[bandIdx] += (float)(10 * Math.Log10(fftData[i]));
					if (float.IsInfinity(spectrum[bandIdx]) || float.IsNaN(spectrum[bandIdx]))
						return null;
					counts[bandIdx]++;
				}
			}

			for (int i = 0; i < spectrum.Length; ++i)
				if (counts[i] > 0)
					spectrum[i] /= counts[i];
			return spectrum;
		}

		private float[] DecimateFreqs(int samplesPerSecond, float[] fftData, int count)
		{
			short[] counts = new short[Params.Count];
			float[] spectrum = new float[Params.Count];

			for (int i = 1; i < count; ++i)
			{
				double freq = samplesPerSecond * i / count / 2;
				int bandIdx = Params.GetBandIdx(freq);
				if (bandIdx >= 0)
				{
					spectrum[bandIdx] += (float)(10 * Math.Log10(fftData[i]));
					if (float.IsInfinity(spectrum[bandIdx]) || float.IsNaN(spectrum[bandIdx]))
						return null;
					counts[bandIdx]++;
				}
			}

			for (int i = 0; i < spectrum.Length; ++i)
				if (counts[i] > 0)
					spectrum[i] /= counts[i];
			return spectrum;
		}
	}
}
